/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <folly/portability/GMock.h>
#include <folly/portability/GTest.h>

#include <thrift/lib/cpp2/Adapt.h>

namespace apache::thrift {
namespace {

enum class OpType { Adapted, Adapter };
struct OpTracker {
  MOCK_METHOD(void, equal, (OpType));
  MOCK_METHOD(void, less, (OpType));
  MOCK_METHOD(void, toThrift, ());
  MOCK_METHOD(void, fromThrift, ());
};

struct MinType {
  OpTracker* tracker;
  int value;
};

struct FullType {
  OpTracker* tracker;
  int value;

 private:
  friend bool operator==(const FullType& lhs, const FullType& rhs) {
    lhs.tracker->equal(OpType::Adapted);
    return lhs.value == rhs.value;
  }
  friend bool operator<(const FullType& lhs, const FullType& rhs) {
    lhs.tracker->less(OpType::Adapted);
    return lhs.value < rhs.value;
  }
};

struct MinAdapter {
  static OpTracker* tracker;

  template <typename T>
  static auto& toThrift(const T& val) {
    tracker->toThrift();
    return val.value;
  }

  template <typename T>
  static T fromThrift(int value) {
    tracker->fromThrift();
    return {tracker, value};
  }
};

OpTracker* MinAdapter::tracker = nullptr;

struct FullAdapter : MinAdapter {
  template <typename T>
  static bool equal(const T& lhs, const T& rhs) {
    tracker->equal(OpType::Adapter);
    return lhs.value == rhs.value;
  }

  template <typename T>
  static bool less(const T& lhs, const T& rhs) {
    tracker->less(OpType::Adapter);
    return lhs.value < rhs.value;
  }
};

class AdaptTest : public ::testing::Test {
 protected:
  testing::StrictMock<OpTracker> tracker_;

  void SetUp() override { MinAdapter::tracker = &tracker_; }
  void TearDown() override { MinAdapter::tracker = nullptr; }

  template <typename T = MinType>
  T val(int value) {
    return T{&tracker_, value};
  }

  template <typename Adapter, typename Adapted>
  bool equal(int lhs, int rhs) {
    return adapt_detail::equal<Adapter>(val<Adapted>(lhs), val<Adapted>(rhs));
  }
  template <typename Adapter, typename Adapted>
  bool notEqual(int lhs, int rhs) {
    return adapt_detail::not_equal<Adapter>(
        val<Adapted>(lhs), val<Adapted>(rhs));
  }
  template <typename Adapter, typename Adapted>
  bool less(int lhs, int rhs) {
    return adapt_detail::less<Adapter>(val<Adapted>(lhs), val<Adapted>(rhs));
  }
};

TEST_F(AdaptTest, Equal) {
  // Uses the thrift type.
  EXPECT_CALL(tracker_, toThrift()).Times(4);
  EXPECT_TRUE((equal<MinAdapter, MinType>(1, 1)));
  EXPECT_FALSE((equal<MinAdapter, MinType>(1, 2)));
  testing::Mock::VerifyAndClearExpectations(&tracker_);

  // Uses operator==()
  EXPECT_CALL(tracker_, equal(OpType::Adapted)).Times(2);
  EXPECT_TRUE((equal<MinAdapter, FullType>(1, 1)));
  EXPECT_FALSE((equal<MinAdapter, FullType>(1, 2)));
  testing::Mock::VerifyAndClearExpectations(&tracker_);

  // Uses adpater.
  EXPECT_CALL(tracker_, equal(OpType::Adapter)).Times(2);
  EXPECT_TRUE((equal<FullAdapter, FullType>(1, 1)));
  EXPECT_FALSE((equal<FullAdapter, FullType>(1, 2)));
  testing::Mock::VerifyAndClearExpectations(&tracker_);
}

TEST_F(AdaptTest, NotEqual) {
  // Uses !operator==()
  EXPECT_CALL(tracker_, equal(OpType::Adapted)).Times(2);
  EXPECT_FALSE((notEqual<MinAdapter, FullType>(1, 1)));
  EXPECT_TRUE((notEqual<MinAdapter, FullType>(1, 2)));
  testing::Mock::VerifyAndClearExpectations(&tracker_);
}

TEST_F(AdaptTest, Less) {
  // Uses the thrift type.
  EXPECT_CALL(tracker_, toThrift()).Times(4);
  EXPECT_TRUE((less<MinAdapter, MinType>(1, 2)));
  EXPECT_FALSE((less<MinAdapter, MinType>(1, 1)));
  testing::Mock::VerifyAndClearExpectations(&tracker_);

  // Uses operator<()
  EXPECT_CALL(tracker_, less(OpType::Adapted)).Times(2);
  EXPECT_TRUE((less<MinAdapter, FullType>(1, 2)));
  EXPECT_FALSE((less<MinAdapter, FullType>(1, 1)));
  testing::Mock::VerifyAndClearExpectations(&tracker_);

  // Uses adpater.
  EXPECT_CALL(tracker_, less(OpType::Adapter)).Times(2);
  EXPECT_TRUE((less<FullAdapter, FullType>(1, 2)));
  EXPECT_FALSE((less<FullAdapter, FullType>(1, 1)));
  testing::Mock::VerifyAndClearExpectations(&tracker_);
}

TEST_F(AdaptTest, Set) {
  EXPECT_CALL(tracker_, toThrift()).Times(::testing::AtLeast(1));

  std::set<MinType, adapt_detail::adapter_less<MinAdapter, MinType>> my_set;
  my_set.emplace(val(1));
  my_set.emplace(val(3));
  EXPECT_EQ(my_set.size(), 2);
  EXPECT_TRUE(my_set.find(val(0)) == my_set.end());
  EXPECT_TRUE(my_set.find(val(1)) != my_set.end());
  EXPECT_TRUE(my_set.find(val(2)) == my_set.end());
  EXPECT_TRUE(my_set.find(val(3)) != my_set.end());
  EXPECT_TRUE(my_set.find(val(4)) == my_set.end());
}

} // namespace
} // namespace apache::thrift
